Skip to content

Comments

fix: match installed skills by source repo, not just name#205

Open
mwmdev wants to merge 2 commits intospacedriveapp:mainfrom
mwmdev:fix/skill-installed-status
Open

fix: match installed skills by source repo, not just name#205
mwmdev wants to merge 2 commits intospacedriveapp:mainfrom
mwmdev:fix/skill-installed-status

Conversation

@mwmdev
Copy link

@mwmdev mwmdev commented Feb 24, 2026

Summary

  • Fixes Bug: After installing a skill, ALL skills with that name get marked as "installed" #190: after installing a skill from the registry, all skills with the same name were marked as installed regardless of source repository
  • Stores source_repo (e.g. anthropics/skills) in SKILL.md frontmatter during GitHub install
  • Surfaces source_repo through the API and uses source_repo/name as a composite key for installed-status checks in the frontend
  • Falls back to name-only matching for skills installed before this fix

Test plan

  • cargo test --lib skills — all 14 tests pass (including 5 new tests for inject_source_repo)
  • Install a skill from the registry, verify SKILL.md gets source_repo in frontmatter
  • In the UI, search for a skill name with multiple results — only the installed one shows the checkmark

Note

Changes overview: Adds source_repo tracking throughout the skill system. Backend now captures GitHub org/repo during installation via inject_source_repo() helper that safely modifies SKILL.md frontmatter (handles missing/malformed frontmatter gracefully). Frontend UI updated to build composite keys (source_repo/name) for installed-skill lookups, with fallback to name-only matching for backward compatibility. API surfaces the new optional source_repo field. All types (Skill, SkillInfo, SkillSet) updated to carry this metadata.

Written by Tembo for commit 826f255. This will update automatically on new commits.

@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

Walkthrough

Adds optional source_repo to skill metadata across backend, API, core model, installer, and frontend. Installer injects provenance into SKILL.md for GitHub installs. Frontend uses composite keys (source_repo/name) to disambiguate installed skills.

Changes

Cohort / File(s) Summary
API Interface
interface/src/api/client.ts
Added optional source_repo?: string to exported SkillInfo.
Frontend: installed detection
interface/src/routes/AgentSkills.tsx
Replaced name-only installed lookup with installedKeys using composite key ${source_repo}/${name} (lowercased) and fallback to name; registry rendering and install/remove checks updated to use composite keys.
Backend: API layer
src/api/skills.rs
Added source_repo: Option<String> to returned SkillInfo (with skip_serializing_if) and populate from skill source.
Core model & tests
src/skills.rs
Added pub source_repo: Option<String> to Skill and SkillInfo; parse frontmatter into source_repo; updated tests to initialize source_repo: None where needed.
Installer & provenance injection
src/skills/installer.rs
Extended extract_and_install signature to accept source_repo: Option<&str>; GitHub installer builds owner/repo and passes it; added inject_source_repo(content, repo) to add/update source_repo in SKILL.md frontmatter (handles absent/malformed frontmatter); IO issues logged as warnings; tests added/updated for injection and parsing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main fix: changing how installed skills are matched from name-only to source repo + name composite key.
Description check ✅ Passed The description clearly explains the fix for issue #190, detailing how source_repo tracking is implemented across backend and frontend components.
Linked Issues check ✅ Passed The PR directly addresses issue #190 by implementing composite key matching (source_repo/name) instead of name-only matching, with backward compatibility for existing installations.
Out of Scope Changes check ✅ Passed All changes directly support the fix for #190: adding source_repo field to types, implementing injection into frontmatter, and updating frontend matching logic.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/skills/installer.rs (1)

36-41: ⚠️ Potential issue | 🟠 Major

No timeout on the GitHub archive download.

reqwest::Client::new() uses no timeout; a stalled or slow response from GitHub will hang the install task indefinitely. The registry proxy in src/api/skills.rs already uses .timeout(Duration::from_secs(10)) as a precedent.

🔧 Proposed fix
-    let client = reqwest::Client::new();
-    let response = client
-        .get(&download_url)
-        .send()
+    let client = reqwest::Client::builder()
+        .timeout(std::time::Duration::from_secs(60))
+        .build()
+        .context("failed to build HTTP client")?;
+    let response = client
+        .get(&download_url)
+        .send()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/skills/installer.rs` around lines 36 - 41, The download currently uses
reqwest::Client::new() with no timeout which can hang; replace the client
creation with a timed client (use
reqwest::Client::builder().timeout(Duration::from_secs(10)).build()) and use
that client for the existing client.get(&download_url).send().await call, adding
a std::time::Duration import if missing so the GitHub archive download in
installer.rs times out like the registry proxy does.
🧹 Nitpick comments (2)
interface/src/routes/AgentSkills.tsx (2)

234-240: Core composite-key logic is correct; document the known limitation of the name-only fallback.

The key shapes match correctly:

  • New installs: installedKeys entry = "${source_repo}/${name}" (e.g. "anthropics/skills/weather"); compositeKey = "${skill.source}/${skill.name}" → same shape → correct match.
  • Different source, same name: keys differ → no false positive → fixes #190 ✓.
  • Legacy installs (no source_repo): installedKeys entry = "weather" → the fallback installedKeys.has(skill.name.toLowerCase()) marks all registry skills with that name as installed, which is the original bug behaviour.

This is intentional per the PR description, but it is worth a brief inline comment so future contributors understand why the fallback exists and the trade-off it carries:

📝 Suggested documentation comment
+	// installedKeys uses source_repo/name when available (new installs) for
+	// per-repo precision. Skills installed before this change have no source_repo
+	// and fall back to name-only matching, which re-exhibits the original false-
+	// positive behaviour until those skills are re-installed.
 	const installedKeys = new Set(
 		installedSkills.map((s) =>
 			s.source_repo
 				? `${s.source_repo}/${s.name}`.toLowerCase()
 				: s.name.toLowerCase(),
 		),
 	);

Also applies to: 371-374

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/AgentSkills.tsx` around lines 234 - 240, Add a brief
inline comment next to the installedKeys construction and the fallback
membership check explaining that legacy installs (installedSkills entries
without source_repo) are stored as name-only (e.g., "weather"), and the fallback
installedKeys.has(skill.name.toLowerCase()) will therefore mark any registry
skill with the same name as installed; state this is an intentional trade-off to
preserve legacy behavior and reference the compositeKey
(`${skill.source}/${skill.name}`) versus name-only key formats so future
contributors understand the limitation and why the fallback exists.

506-517: key={skill.name} on InstalledSkill is safe today but fragile.

SkillSet already enforces one-skill-per-lowercase-name, so duplicate keys cannot occur now. However, if that invariant ever relaxes (e.g. a future multi-source install mode), React will silently render only the last matching child.

Consider key={skill.source_repo ? \${skill.source_repo}/${skill.name}` : skill.name}` to make the key stable and unique under any future extension.

✏️ Proposed change
-								key={skill.name}
+								key={skill.source_repo ? `${skill.source_repo}/${skill.name}` : skill.name}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interface/src/routes/AgentSkills.tsx` around lines 506 - 517, The current
InstalledSkill list uses key={skill.name} which relies on a fragile uniqueness
invariant; change the key generation in the installedSkills.map to a stable
unique composite (e.g., include skill.source_repo when present) so keys remain
unique if multiple skills share a name. Update the InstalledSkill mapping to
compute the key as something like `${skill.source_repo}/${skill.name}` when
skill.source_repo exists, otherwise fallback to skill.name, ensuring
InstalledSkill receives that composite key and preventing React child key
collisions in future multi-source installs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/skills/installer.rs`:
- Around line 184-198: The read of SKILL.md currently swallows errors with `if
let Ok(content) = fs::read_to_string(&skill_md).await`, so add error
handling/logging for the read path: replace the `if let Ok(...)` with a match or
`if let Err(e)` branch so that when `fs::read_to_string(&skill_md).await` fails
you emit a `tracing::warn!` (including `skill = %skill_name`, `%e`, and a clear
message like "failed to read SKILL.md") before returning/continuing; keep the
existing logic that calls `inject_source_repo(&content, repo)` and logs write
failures for `fs::write(&skill_md, patched).await`.

---

Outside diff comments:
In `@src/skills/installer.rs`:
- Around line 36-41: The download currently uses reqwest::Client::new() with no
timeout which can hang; replace the client creation with a timed client (use
reqwest::Client::builder().timeout(Duration::from_secs(10)).build()) and use
that client for the existing client.get(&download_url).send().await call, adding
a std::time::Duration import if missing so the GitHub archive download in
installer.rs times out like the registry proxy does.

---

Nitpick comments:
In `@interface/src/routes/AgentSkills.tsx`:
- Around line 234-240: Add a brief inline comment next to the installedKeys
construction and the fallback membership check explaining that legacy installs
(installedSkills entries without source_repo) are stored as name-only (e.g.,
"weather"), and the fallback installedKeys.has(skill.name.toLowerCase()) will
therefore mark any registry skill with the same name as installed; state this is
an intentional trade-off to preserve legacy behavior and reference the
compositeKey (`${skill.source}/${skill.name}`) versus name-only key formats so
future contributors understand the limitation and why the fallback exists.
- Around line 506-517: The current InstalledSkill list uses key={skill.name}
which relies on a fragile uniqueness invariant; change the key generation in the
installedSkills.map to a stable unique composite (e.g., include
skill.source_repo when present) so keys remain unique if multiple skills share a
name. Update the InstalledSkill mapping to compute the key as something like
`${skill.source_repo}/${skill.name}` when skill.source_repo exists, otherwise
fallback to skill.name, ensuring InstalledSkill receives that composite key and
preventing React child key collisions in future multi-source installs.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af095f3 and 826f255.

📒 Files selected for processing (5)
  • interface/src/api/client.ts
  • interface/src/routes/AgentSkills.tsx
  • src/api/skills.rs
  • src/skills.rs
  • src/skills/installer.rs

@mwmdev mwmdev force-pushed the fix/skill-installed-status branch from edc37b4 to 268c1a2 Compare February 25, 2026 07:37
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/skills/installer.rs`:
- Around line 232-259: The current final assembly uses
format!("---{new_fm}\n---{body}") which concatenates the closing frontmatter
delimiter and the body without ensuring a separating newline; update the
assembly to always emit the delimiter followed by a newline before body (e.g.,
format!("---\n{new_fm}\n---\n{body}")), ensuring newlines around the
opening/closing '---' and referencing the same new_fm and body variables so
frontmatter parsing isn't broken.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edc37b4 and 268c1a2.

📒 Files selected for processing (5)
  • interface/src/api/client.ts
  • interface/src/routes/AgentSkills.tsx
  • src/api/skills.rs
  • src/skills.rs
  • src/skills/installer.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/api/skills.rs
  • interface/src/routes/AgentSkills.tsx
  • interface/src/api/client.ts
  • src/skills.rs

@mwmdev mwmdev force-pushed the fix/skill-installed-status branch from 268c1a2 to 6e40aba Compare February 25, 2026 07:58
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/skills/installer.rs (2)

382-438: Good test coverage for inject_source_repo — edge cases and roundtrip with parse_frontmatter are well covered.

One gap worth noting: there's no test for content with leading whitespace before the frontmatter delimiter (e.g. " ---\nname: x\n---\n"). trim_start() on line 224 handles this, but a test would document the behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/skills/installer.rs` around lines 382 - 438, Add a new unit test that
verifies inject_source_repo correctly handles leading whitespace before the
frontmatter delimiter: create content with leading spaces before the initial
"---" (e.g. "  ---\nname: x\n---\n\n# Body\n"), call inject_source_repo(content,
"owner/repo"), and assert the returned string contains a single source_repo:
owner/repo inside the frontmatter (use parse_frontmatter or string checks),
preserves the existing fields (like name: x) and retains the body "# Body".
Reference inject_source_repo and parse_frontmatter to locate where the test
should sit alongside the other inject_source_repo tests.

222-259: inject_source_repo: minor leading-blank-line artifact in rewritten frontmatter.

When fm_block starts with \n (which it always does, since after_opening begins right after the opening ---), the first element from .lines() is "". After join("\n"), new_fm starts with an empty line, so the output looks like:

---
                          ← blank line here
name: weather
source_repo: anthropics/skills
---

This is functionally harmless — parse_frontmatter trims and skips blank lines — but it's a minor aesthetic wart. Consider stripping the leading newline from fm_block before splitting:

♻️ Optional cleanup
-    let fm_block = &after_opening[..end_pos];
+    let fm_block = after_opening[..end_pos].trim_start_matches('\n');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/skills/installer.rs` around lines 222 - 259, The inject_source_repo
function produces a leading blank line in the rewritten frontmatter because
fm_block begins with a newline; fix it by normalizing fm_block before splitting
(e.g., remove a leading '\n' or use trim_start_matches on fm_block) so that
.lines() doesn't yield an empty first element, then proceed with the existing
filtering/joining logic to append the new source_repo line.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/skills/installer.rs`:
- Around line 382-438: Add a new unit test that verifies inject_source_repo
correctly handles leading whitespace before the frontmatter delimiter: create
content with leading spaces before the initial "---" (e.g. "  ---\nname:
x\n---\n\n# Body\n"), call inject_source_repo(content, "owner/repo"), and assert
the returned string contains a single source_repo: owner/repo inside the
frontmatter (use parse_frontmatter or string checks), preserves the existing
fields (like name: x) and retains the body "# Body". Reference
inject_source_repo and parse_frontmatter to locate where the test should sit
alongside the other inject_source_repo tests.
- Around line 222-259: The inject_source_repo function produces a leading blank
line in the rewritten frontmatter because fm_block begins with a newline; fix it
by normalizing fm_block before splitting (e.g., remove a leading '\n' or use
trim_start_matches on fm_block) so that .lines() doesn't yield an empty first
element, then proceed with the existing filtering/joining logic to append the
new source_repo line.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 268c1a2 and a983375.

📒 Files selected for processing (5)
  • interface/src/api/client.ts
  • interface/src/routes/AgentSkills.tsx
  • src/api/skills.rs
  • src/skills.rs
  • src/skills/installer.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/api/skills.rs
  • interface/src/api/client.ts
  • src/skills.rs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: After installing a skill, ALL skills with that name get marked as "installed"

1 participant